#include "checksum.h"
#include "common.h"
#include "eui64.h"
#include "../lookup/lookup.h"
#include "../protocol/protocol.h"
#include "router_hal.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

uint8_t packet[2048];
uint8_t output[2048];
#define METRIC_INF 16
// for online experiment, don't change
#ifdef ROUTER_R1
// 0: fd00::1:1/112
// 1: fd00::3:1/112
// 2: fd00::6:1/112
// 3: fd00::7:1/112
in6_addr addrs[N_IFACE_ON_BOARD] = {
    {0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x01, 0x00, 0x01},
    {0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x03, 0x00, 0x01},
    {0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x06, 0x00, 0x01},
    {0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x07, 0x00, 0x01},
};
#elif defined(ROUTER_R2)
// 0: fd00::3:2/112
// 1: fd00::4:1/112
// 2: fd00::8:1/112
// 3: fd00::9:1/112
in6_addr addrs[N_IFACE_ON_BOARD] = {
    {0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x03, 0x00, 0x02},
    {0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x04, 0x00, 0x01},
    {0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x08, 0x00, 0x01},
    {0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x09, 0x00, 0x01},
};
#elif defined(ROUTER_R3)
// 0: fd00::4:2/112
// 1: fd00::5:2/112
// 2: fd00::a:1/112
// 3: fd00::b:1/112
in6_addr addrs[N_IFACE_ON_BOARD] = {
    {0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x04, 0x00, 0x02},
    {0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x05, 0x00, 0x02},
    {0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x0a, 0x00, 0x01},
    {0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x0b, 0x00, 0x01},
};
#else

// 自己调试用，你可以按需进行修改
// 0: fd00::0:1
// 1: fd00::1:1
// 2: fd00::2:1
// 3: fd00::3:1
in6_addr addrs[N_IFACE_ON_BOARD] = {
        {0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                0x00, 0x00, 0x00, 0x01},
        {0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                0x00, 0x01, 0x00, 0x01},
        {0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                0x00, 0x02, 0x00, 0x01},
        {0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                0x00, 0x03, 0x00, 0x01},
};
#endif

uint32_t make_ip6_udp_packet(in6_addr dst, uint8_t if_index, uint16_t uh_dport, uint16_t uh_sport, uint8_t *payload,
                             uint16_t payload_len) {
    ether_addr mac;
    HAL_GetInterfaceMacAddress(if_index, &mac);
    // 构造 IPv6 packet

    // IPv6 header
    ip6_hdr *reply_ip6 = (ip6_hdr *) &output[0];
    // flow label
    reply_ip6->ip6_flow = 0;
    // version
    reply_ip6->ip6_vfc = 6 << 4;
    // payload length
    // ip6->ip6_plen = htons(???);
    reply_ip6->ip6_plen = htons(payload_len + sizeof(udphdr));
    // next header
    reply_ip6->ip6_nxt = IPPROTO_UDP;
    // hop limit
    reply_ip6->ip6_hlim = 255;
    // src ip
    reply_ip6->ip6_src = eui64(mac);
    // dst ip
    reply_ip6->ip6_dst = dst;

    udphdr *udp = (udphdr *) &output[sizeof(ip6_hdr)];
    // dst port
    udp->uh_dport = htons(uh_dport);
    // src port
    udp->uh_sport = htons(uh_sport);

    udp->uh_ulen = htons(payload_len + sizeof(udphdr));
    udp->uh_sum = 0;

    memcpy(&output[sizeof(ip6_hdr) + sizeof(udphdr)], payload, payload_len);
    uint32_t len = sizeof(ip6_hdr) + sizeof(udphdr) + payload_len;
    validateAndFillChecksum(&output[0], len);
    return len;
}

uint32_t make_ip6_icmp_packet(in6_addr dst, uint8_t if_index, uint8_t type, uint8_t code, uint8_t *payload,
                              uint16_t payload_len) {
    ether_addr mac;
    HAL_GetInterfaceMacAddress(if_index, &mac);
    // 构造 IPv6 packet

    // IPv6 header
    ip6_hdr *reply_ip6 = (ip6_hdr *) &output[0];
    // flow label
    reply_ip6->ip6_flow = 0;
    // version
    reply_ip6->ip6_vfc = 6 << 4;
    // payload length
    // ip6->ip6_plen = htons(???);
    reply_ip6->ip6_plen = htons(payload_len + sizeof(icmp6_hdr));
    // next header
    reply_ip6->ip6_nxt = IPPROTO_ICMPV6;
    // hop limit
    reply_ip6->ip6_hlim = 255;
    // src ip
    reply_ip6->ip6_src = addrs[if_index];
    // dst ip
    reply_ip6->ip6_dst = dst;

    icmp6_hdr *icmp = (icmp6_hdr *) &output[sizeof(ip6_hdr)];

    icmp->icmp6_type = type;
    icmp->icmp6_code = code;
    icmp->icmp6_cksum = 0;
    icmp->icmp6_dataun = {0};
    memcpy(&output[sizeof(ip6_hdr) + sizeof(icmp6_hdr)], payload, payload_len);
    uint32_t len = sizeof(ip6_hdr) + sizeof(icmp6_hdr) + payload_len;
    validateAndFillChecksum(&output[0], len);
    return len;
}

int make_ripng_packets(RipngPacket *ripng_packet, int lo, int hi, int if_index) {
    in6_addr next_hop = addrs[if_index];
    auto lookup_table = get_lookup_table();
//    printf("next_hop: %s\n", inet6_ntoa(next_hop));
    for (int i = lo; i < hi; ++i) {
        ripng_packet->entries[i - lo].prefix_len = (uint8_t) lookup_table[i].len;
        ripng_packet->entries[i - lo].route_tag = 0;
        ripng_packet->entries[i - lo].prefix_or_nh = lookup_table[i].addr;
        // Split Horizon with Poisoned Reverse
//        bool poisoned_reverse = lookup_table[i].nexthop == next_hop ||
//                                (lookup_table[i].nexthop == in6_addr{0} && lookup_table[i].if_index == if_index);
        bool poisoned_reverse = lookup_table[i].nexthop == next_hop;
        if (poisoned_reverse) {
            ripng_packet->entries[i - lo].metric = METRIC_INF;
        } else {
            ripng_packet->entries[i - lo].metric = lookup_table[i].metric;
        }
    }
    return 0;
}

int main(int argc, char *argv[]) {
    // 初始化 HAL
    int res = HAL_Init(1, addrs);
    if (res < 0) {
        return res;
    }

    printf("HAL Inited!\n");
    // 插入直连路由
    // 例如 R2：
    // fd00::3:0/112 if 0
    // fd00::4:0/112 if 1
    // fd00::8:0/112 if 2
    // fd00::9:0/112 if 3
    for (uint32_t i = 0; i < N_IFACE_ON_BOARD; i++) {
        in6_addr mask = len_to_mask(112);
        // TODO（1 行）
        // 这里需要添加额外的字段来初始化 metric
        RoutingTableEntry entry = {
                .addr = addrs[i] & mask,
                .len = 112,
                .if_index = i,
                .nexthop = in6_addr{0}, // 全 0 表示直连路由
                .metric = 1 // metric scope should be [1, 16]
        };
        update(true, entry);
    }

    uint64_t last_time = 0;
    while (1) {
        uint64_t time = HAL_GetTicks();
//        if (time > last_time + 60 * 1000) {
//            display_route_table();
//        }

        // RFC 要求每 30s 发送一次
        // 为了提高收敛速度，设为 5s
        if (time > last_time + 5 * 1000) {
//            display_route_table();
            // 提示：你可以打印完整的路由表到 stdout/stderr 来帮助调试。
            printf("5s Timer\n");

            for (int i = 0; i < N_IFACE_ON_BOARD; i++) {
                // TODO（40 行）
                // 这一步需要向所有 interface 发送当前的完整路由表，设置 Command 为
                // Response，并且注意当路由表表项较多时，需要拆分为多个 IPv6
                // packet。此时 IPv6 packet 的源地址应为使用 eui64 计算得到的 Link Local
                // 地址，目的地址为 ff02::9，以太网帧的源 MAC 地址为当前 interface 的
                // MAC 地址，目的 MAC 地址为 33:33:00:00:00:09，详见 RFC 2080
                // Section 2.5.2 Generating Response Messages。
                //
                // 注意需要实现水平分割以及毒性反转（Split Horizon with Poisoned
                // Reverse） 即，如果某一条路由表项是从 interface A 学习到的，那么发送给
                // interface A 的 RIPng 表项中，该项的 metric 设为 16。详见 RFC 2080
                // Section 2.6 Split Horizon。因此，发往各个 interface 的 RIPng
                // 表项是不同的。
                ether_addr dst_mac;
                get_ipv6_mcast_mac(addrs[i], &dst_mac);

                auto lookupTable = get_lookup_table();
                for (int j = 0; j < lookupTable.size() / RIPNG_MAX_RTE + 1; ++j) {
                    RipngPacket rip_packet;
                    uint8_t *buf = (uint8_t *) malloc(2048);
                    int lo = j * RIPNG_MAX_RTE;
                    int hi = min((int) lookupTable.size(), lo + RIPNG_MAX_RTE);
                    make_ripng_packets(&rip_packet, lo, hi, i);

                    rip_packet.numEntries = hi - lo;
                    rip_packet.command = 2;
                    uint32_t plen = assemble(&rip_packet, buf);
                    uint32_t len = make_ip6_udp_packet(inet6_pton("ff02::9"), i, 521, 521,
                                                       buf, plen);
                    HAL_SendIPPacket(i, output, len, dst_mac);
                    free(buf);
                }
            }
            last_time = time;
        }

        int mask = (1 << N_IFACE_ON_BOARD) - 1;
        ether_addr src_mac;
        ether_addr dst_mac;
        int if_index;
        res = HAL_ReceiveIPPacket(mask, packet, sizeof(packet), &src_mac, &dst_mac,
                                  1000, &if_index);
        if (res == HAL_ERR_EOF) {
            break;
        } else if (res < 0) {
            return res;
        } else if (res == 0) {
            // Timeout
            continue;
        } else if (res > sizeof(packet)) {
            // packet is truncated, ignore it
            continue;
        }

        // 检查 IPv6 头部长度
        ip6_hdr *ip6 = (ip6_hdr *) packet;
        if (res < sizeof(ip6_hdr)) {
            printf("Received invalid ipv6 packet (%d < %lu)\n", res, sizeof(ip6_hdr));
            continue;
        }
        uint16_t plen = htons(ip6->ip6_plen);
        if (res < plen + sizeof(ip6_hdr)) {
            printf("Received invalid ipv6 packet (%d < %d + %lu)\n", res, plen,
                   sizeof(ip6_hdr));
            continue;
        }

        // 检查 IPv6 头部目的地址是否为我自己
        bool dst_is_me = false;
        for (int i = 0; i < N_IFACE_ON_BOARD; i++) {
            if (memcmp(&ip6->ip6_dst, &addrs[i], sizeof(in6_addr)) == 0) {
                dst_is_me = true;
                break;
            }
        }

        // TODO（1 行）
        // 修改这个检查，当目的地址为 RIPng 的组播目的地址（ff02::9）时也设置
        // dst_is_me 为 true。
        if (strcmp(inet6_ntoa(ip6->ip6_dst), "ff02::9") == 0) {
            dst_is_me = true;
        }

        if (dst_is_me) {
            // 目的地址是我，按照类型进行处理

            // 检查 checksum 是否正确
            if (ip6->ip6_nxt == IPPROTO_UDP || ip6->ip6_nxt == IPPROTO_ICMPV6) {
                if (!validateAndFillChecksum(packet, res)) {
                    printf("Received packet with bad checksum\n");
                    continue;
                }
            }

            if (ip6->ip6_nxt == IPPROTO_UDP) {
                // 检查是否为 RIPng packet
                RipngPacket ripng;
                RipngErrorCode err = disassemble(packet, res, &ripng);
                if (err == SUCCESS) {
                    if (ripng.command == 1) {
                        // 可选功能，实现了可以加快路由表收敛速度
                        // Command 为 Request
                        // 参考 RFC 2080 Section 2.4.1 Request Messages 实现
                        // 本次实验中，可以简化为只考虑输出完整路由表的情况

                        RipngPacket resp;
                        // 与 5s Timer 时的处理类似，也需要实现水平分割和毒性反转
                        // 可以把两部分代码写到单独的函数中
                        // 不同的是，在 5s Timer
                        // 中要组播发给所有的路由器；这里则是某一个路由器 Request
                        // 本路由器，因此回复 Response 的时候，目的 IPv6 地址和 MAC
                        // 地址都应该指向发出请求的路由器

                        // 最后把 RIPng 报文发送出去
                        printf("receive a request\n");
                    } else {
//                        printf("receive a packet\n");

                        // TODO（40 行）
                        // Command 为 Response
                        // 参考 RFC 2080 Section 2.4.2 Request Messages 实现
                        // 按照接受到的 RIPng 表项更新自己的路由表

                        for (int i = 0; i < ripng.numEntries; ++i) {
//                            printf("prefix: %s, len: %d, metric: %d, route_tag: %d\n",
//                                   inet6_ntoa(ripng.entries[i].prefix_or_nh),
//                                   ripng.entries[i].prefix_len, ripng.entries[i].metric, ripng.entries[i].route_tag);
                            RoutingTableEntry entry;
                            // 接下来的处理中，都首先对输入的 RIPng 表项做如下处理：
                            // metric = MIN(metric + cost, infinity)
                            // 其中 cost 取 1，表示经过了一跳路由器；infinity 用 16 表示
                            entry.metric = min(ripng.entries[i].metric + 1, METRIC_INF);
                            entry.nexthop = ip6->ip6_src;
                            entry.len = ripng.entries[i].prefix_len;
                            entry.addr = ripng.entries[i].prefix_or_nh;
                            entry.if_index = if_index;

                            // 在本实验中，可以忽略 metric=0xFF 的表项，它表示的是 Nexthop
                            // 的设置，可以忽略
                            if (ripng.entries[i].metric == 0xFF) {
                                continue;
                            }

                            RoutingTableEntry old_entry;

                            if (!exact_query(entry.addr, entry.len, &old_entry)) {
                                // 如果出现了一条新的路由表项，并且 metric 不等于 16：
                                // 插入到自己的路由表中，设置 nexthop
                                // 地址为发送这个 Response 的路由器。
                                if (entry.metric != METRIC_INF) {
                                    update(true, entry);
                                }
                            } else {
                                // 如果收到的路由表项和已知的重复（注意，是精确匹配），
                                // 进行以下的判断：如果路由表中的表项是之前从该路由器从学习而来，那么直接更新
                                // metric
                                // 为新的值；如果路由表中表现是从其他路由器那里学来，就比较已有的表项和
                                // RIPng 表项中的 metric 大小，如果 RIPng 表项中的 metric
                                // 更小，说明找到了一条更新的路径，那就用新的表项替换原有的，同时更新
                                // nexthop 地址。
                                if (entry.nexthop == old_entry.nexthop && entry.if_index == old_entry.if_index) {
                                    update(true, entry);
                                } else {
                                    if (entry.metric < old_entry.metric) {
                                        update(true, entry);
                                    }
                                }
                            }
                        }
                        // 可选功能：实现 Triggered
                        // Updates，即在路由表出现更新的时候，向所有 interface
                        // 发送出现变化的路由表项，注意此时依然要实现水平分割和毒性反转。详见
                        // RFC 2080 Section 2.5.1。

                    }
                } else {
                    // 接受到一个错误的 RIPng packet >_<
                    printf("Got bad RIPng packet from IP %s with error: %s\n",
                           inet6_ntoa(ip6->ip6_src), ripng_error_to_string(err));
                }
            } else if (ip6->ip6_nxt == IPPROTO_ICMPV6) {
                // TODO（20 行）
                // 如果是 ICMPv6 packet
                // 检查是否是 Echo Request
                printf("receive icmpv6\n");
                icmp6_hdr *icmp6 = (struct icmp6_hdr *) &packet[sizeof(struct ip6_hdr)];
                if (icmp6->icmp6_type == 128) {
                    // 如果是 Echo Request，生成一个对应的 Echo Reply：交换源和目的 IPv6
                    // 地址，设置 type 为 Echo Reply，设置 TTL（Hop Limit） 为
                    // 64，重新计算 Checksum 并发送出去。详见 RFC 4443 Section 4.2 Echo
                    // Reply Message
                    printf("icmp6_type: %d, src: %s, dst: %s\n", icmp6->icmp6_type, inet6_ntoa(ip6->ip6_src),
                           inet6_ntoa(ip6->ip6_dst));
                    icmp6->icmp6_type = 129;
                    in6_addr addr = ip6->ip6_dst;
                    ip6->ip6_dst = ip6->ip6_src;
                    ip6->ip6_src = addr;
                    ip6->ip6_hops = 64;
                    icmp6->icmp6_cksum = 0;
                    validateAndFillChecksum(packet, res);
                    HAL_SendIPPacket(if_index, packet, res, src_mac);
                }
            }
            continue;
        } else {
            // 目标地址不是我，考虑转发给下一跳
            // 检查是否是组播地址（ff00::/8），不需要转发组播分组
            if (ip6->ip6_dst.s6_addr[0] == 0xff) {
                printf("Don't forward multicast packet to %s\n",
                       inet6_ntoa(ip6->ip6_dst));
                continue;
            }

            // 检查 TTL（Hop Limit）是否小于或等于 1
            uint8_t ttl = ip6->ip6_hops;
            if (ttl <= 1) {
                // TODO（40 行）
                // 发送 ICMP Time Exceeded 消息
                // 将接受到的 IPv6 packet 附在 ICMPv6 头部之后。
                // 如果长度大于 1232 字节，则取前 1232 字节：
                // 1232 = IPv6 Minimum MTU(1280) - IPv6 Header(40) - ICMPv6 Header(8)
                // 意味着发送的 ICMP Time Exceeded packet 大小不大于 IPv6 Minimum MTU
                // 不会因为 MTU 问题被丢弃。
                // 详见 RFC 4443 Section 3.3 Time Exceeded Message
                // 计算 Checksum 后由自己的 IPv6 地址发送给源 IPv6 地址。
                uint32_t len = sizeof(struct ip6_hdr) + sizeof(icmp6_hdr) + min(1232, res);
                make_ip6_icmp_packet(ip6->ip6_src, if_index, 3, 0, packet, min(1232, res));
                HAL_SendIPPacket(if_index, output, len, src_mac);
            } else {
                // 转发给下一跳
                // 按最长前缀匹配查询路由表
//                printf("dst is not me\n");
                in6_addr nexthop;
                uint32_t dest_if;
                if (prefix_query(ip6->ip6_dst, &nexthop, &dest_if)) {
                    // 找到路由
                    ether_addr dest_mac;
                    // 如果下一跳为全 0，表示的是直连路由，目的机器和本路由器可以直接访问
                    if (nexthop == in6_addr{0}) {
                        nexthop = ip6->ip6_dst;
                    }
//                    printf("nexthop: %s, if: %d\n", inet6_ntoa(nexthop), dest_if);
                    if (HAL_GetNeighborMacAddress(dest_if, nexthop, &dest_mac) == 0) {
                        // 在 NDP 表中找到了下一跳的 MAC 地址
                        // TTL-1
                        ip6->ip6_hops--;

                        // 转发出去
                        memcpy(output, packet, res);
//                        printf("dest_mac: %s\n", ether_ntoa(dest_mac));
                        HAL_SendIPPacket(dest_if, output, res, dest_mac);
                    } else {
                        // 没有找到下一跳的 MAC 地址
                        // 本实验中可以直接丢掉，等对方回复 NDP 之后，再恢复正常转发。
                        printf("Nexthop ip %s is not found in NDP table\n",
                               inet6_ntoa(nexthop));
                    }
                } else {
                    // TODO（40 行）
                    // 没有找到路由
                    // 发送 ICMPv6 Destination Unreachable 消息
                    // 要求与上面发送 ICMPv6 Time Exceeded 消息一致
                    // Code 取 0，表示 No route to destination
                    // 详见 RFC 4443 Section 3.1 Destination Unreachable Message
                    // 计算 Checksum 后由自己的 IPv6 地址发送给源 IPv6 地址。
                    uint32_t len = sizeof(struct ip6_hdr) + sizeof(icmp6_hdr) + min(1232, res);
                    make_ip6_icmp_packet(ip6->ip6_src, if_index, 1, 0, packet, min(1232, res));
                    HAL_SendIPPacket(if_index, output, len, src_mac);
                    printf("Destination IP %s not found in routing table",
                           inet6_ntoa(ip6->ip6_dst));
                    printf(" and source IP is %s\n", inet6_ntoa(ip6->ip6_src));
                }
            }
        }
    }
    return 0;
}
